Изучение поведения пользователей в мобильном приложении "Ненужные вещи"¶

Описание проекта:

Необходимо провести исследовательский анализ данных и проанализировать влиятие событий на совершение целевого события. Также важно выявить какие сценарии использвания приложения выделяются и как различается время между событиями ADVERT_OPEN -> CONTACTS_SHOW И TIPS_CLICK -> CONTACTS_SHOW.Какая конверсия в целевое событие у данных действий?

После нужно проверить следующие статистические гипотезы:

  • Одни пользователи совершают действия tips_show и tips_click, другие — только tips_show. Проверьте гипотезу: конверсия в просмотры контактов различается у этих двух групп.
  • Одни пользователи просматривают фотографии photos_show, другие нет. Проверить гипотезу: пользователи, которые просматривают фотографии, чаще звонят по номеру из объявления (конверсия в звонки у групп различается).

Описание данных:

В датасете содержатся данные пользователей, впервые совершивших действия в приложении после 7 октября 2019 года.

Колонки в mobile_sources.csv:

  • userId — идентификатор пользователя,
  • source — источник, с которого пользователь установил приложение.

Колонки в mobile_dataset.csv: **

  • event.time — время совершения,
  • user.id — идентификатор пользователя,
  • event.name — действие пользователя.

Виды действий:

  • advert_open — открыл карточки объявления,
  • photos_show — просмотрел фотографий в объявлении,
  • tips_show — увидел рекомендованные объявления,
  • tips_click — кликнул по рекомендованному объявлению,
  • contacts_show и show_contacts — посмотрел номер телефона,
  • contacts_call — позвонил по номеру из объявления,
  • map — открыл карту объявлений,
  • search_1—search_7 — разные действия, связанные с поиском по сайту,
  • favorites_add — добавил объявление в избранное.

Навыки и инструменты:

  • python
  • pandas
  • scipy
  • matplotlib
  • numpy
  • cmath
  • предобработка данных
  • исследовательский анализ данных
  • поиск сценарией пользователей
  • диаграмма Санкея
  • статистические гипотезы

Загрузка и изучение данных¶

Загрузка данных¶

In [1]:
import pandas as pd
import numpy as np
import scipy.stats as stats
import seaborn as sns
import matplotlib.pyplot as plt
import datetime as dt
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly.graph_objs as go
import plotly.express as px
import plotly.figure_factory as ff
import warnings

from scipy import stats as st
import numpy as np
import cmath as mth

import os
import re
import requests



import plotly.graph_objects as go
from tqdm.auto import tqdm
%config InlineBackend.figure_format = 'retina'
In [2]:
mobile_sources = pd.read_csv('https://code.s3.yandex.net/datasets/mobile_sources.csv') 
mobile_dataset = pd.read_csv('https://code.s3.yandex.net/datasets/mobile_dataset.csv')

Изучение общей информации¶

In [3]:
mobile_dataset.head(5)
Out[3]:
event.time event.name user.id
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
2 2019-10-07 00:00:02.245341 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c
3 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
4 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c
In [4]:
mobile_dataset.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 3 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   event.time  74197 non-null  object
 1   event.name  74197 non-null  object
 2   user.id     74197 non-null  object
dtypes: object(3)
memory usage: 1.7+ MB

Всего в датасете mobile_dataset 74197 записей. Далее мы изменим названия солбцов и типы данных.

In [5]:
mobile_sources.head(5)
Out[5]:
userId source
0 020292ab-89bc-4156-9acf-68bc2783f894 other
1 cf7eda61-9349-469f-ac27-e5b6f5ec475c yandex
2 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 yandex
3 d9b06b47-0f36-419b-bbb0-3533e582a6cb other
4 f32e1e2a-3027-4693-b793-b7b3ff274439 google
In [6]:
mobile_sources.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4293 entries, 0 to 4292
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   userId  4293 non-null   object
 1   source  4293 non-null   object
dtypes: object(2)
memory usage: 67.2+ KB

В датасете mobile_sources 4293 записей. Далее также поменяем названия столбцов.

Предобработка данных¶

Замена названия столбцов¶

Заменим названия столбцов в датасете mobile_dataset на более удобные.

In [7]:
mobile_dataset.rename(columns = {'event.time':'event_time', 'event.name':'event_name','user.id':'user_id'}, inplace = True )
In [8]:
mobile_dataset.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 3 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   event_time  74197 non-null  object
 1   event_name  74197 non-null  object
 2   user_id     74197 non-null  object
dtypes: object(3)
memory usage: 1.7+ MB

Также заменим названия столбцов в датасете mobile_sources.

In [9]:
mobile_sources.rename(columns = {'userId':'user_id'}, inplace = True )
In [10]:
mobile_sources.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4293 entries, 0 to 4292
Data columns (total 2 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   user_id  4293 non-null   object
 1   source   4293 non-null   object
dtypes: object(2)
memory usage: 67.2+ KB

Также, для удобства исследования соединим два датасета.

In [11]:
data=mobile_sources.merge(mobile_dataset,how='left')
data
Out[11]:
user_id source event_time event_name
0 020292ab-89bc-4156-9acf-68bc2783f894 other 2019-10-07 00:00:00.431357 advert_open
1 020292ab-89bc-4156-9acf-68bc2783f894 other 2019-10-07 00:00:01.236320 tips_show
2 020292ab-89bc-4156-9acf-68bc2783f894 other 2019-10-07 00:00:07.039334 tips_show
3 020292ab-89bc-4156-9acf-68bc2783f894 other 2019-10-07 00:01:27.770232 advert_open
4 020292ab-89bc-4156-9acf-68bc2783f894 other 2019-10-07 00:01:34.804591 tips_show
... ... ... ... ...
74192 d157bffc-264d-4464-8220-1cc0c42f43a9 google 2019-11-03 23:46:47.068179 map
74193 d157bffc-264d-4464-8220-1cc0c42f43a9 google 2019-11-03 23:46:58.914787 advert_open
74194 d157bffc-264d-4464-8220-1cc0c42f43a9 google 2019-11-03 23:47:01.232230 tips_show
74195 d157bffc-264d-4464-8220-1cc0c42f43a9 google 2019-11-03 23:47:47.475102 advert_open
74196 d157bffc-264d-4464-8220-1cc0c42f43a9 google 2019-11-03 23:47:50.087645 tips_show

74197 rows × 4 columns

Замена типов данных¶

Заменим тип данных в формате времени.

In [12]:
data['event_time'] = pd.to_datetime(data['event_time'], format='%Y.%m.%d %H:%M:%S').dt.round('1S')
In [13]:
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 74197 entries, 0 to 74196
Data columns (total 4 columns):
 #   Column      Non-Null Count  Dtype         
---  ------      --------------  -----         
 0   user_id     74197 non-null  object        
 1   source      74197 non-null  object        
 2   event_time  74197 non-null  datetime64[ns]
 3   event_name  74197 non-null  object        
dtypes: datetime64[ns](1), object(3)
memory usage: 2.8+ MB

Проверка на пропуски¶

In [14]:
data.isna().sum()
Out[14]:
user_id       0
source        0
event_time    0
event_name    0
dtype: int64

Пропусков в датасете нет. Посмотрим на дубликаты.

Проверка на дубликаты¶

In [15]:
data.duplicated().sum() 
Out[15]:
1118

Так как выше мы округляли значения времени, возможно могли появиться дубликаты. Оставим их для корректности исследования.

Создание столбца session_id¶

In [16]:
data=data.sort_values(['user_id','event_time'])
data
Out[16]:
user_id source event_time event_name
2171 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:39:46 tips_show
2172 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:40:31 tips_show
2173 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:41:06 tips_show
2174 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:43:21 tips_show
2175 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:45:31 tips_show
... ... ... ... ...
19048 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 15:51:24 tips_show
19049 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 15:51:58 contacts_show
19050 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 16:07:41 tips_show
19051 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 16:08:18 tips_show
19052 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 16:08:25 tips_show

74197 rows × 4 columns

В среднем в аналитике (больше в веб-аналитике) выделяют дефолтный тайм-аут в 30 минут. В целом, можем принять за наш тайм-аут, за это время пользователь сможет просмотреть фото, контакты и даже совершить звонок. По данным Google Analytics (https://support.google.com/analytics/answer/2731565?hl=ru#zippy=%2C%D1%81%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B0%D0%BD%D0%B8%D0%B5).

В целом, значение в 30 секунд выглядит адекватным. Подставим в формулу.

In [17]:
g = (data.groupby('user_id')['event_time'].diff() > pd.Timedelta('30Min')).cumsum()
data['session_id'] = data.groupby(['user_id', g], sort=False).ngroup() + 1
data
Out[17]:
user_id source event_time event_name session_id
2171 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:39:46 tips_show 1
2172 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:40:31 tips_show 1
2173 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:41:06 tips_show 1
2174 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:43:21 tips_show 1
2175 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 2019-10-07 13:45:31 tips_show 1
... ... ... ... ... ...
19048 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 15:51:24 tips_show 10368
19049 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 15:51:58 contacts_show 10368
19050 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 16:07:41 tips_show 10368
19051 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 16:08:18 tips_show 10368
19052 fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 2019-11-03 16:08:25 tips_show 10368

74197 rows × 5 columns

Замена значений в столбцах¶

Заменим одинаковые по смыслу значения в столбцах event_name.

In [18]:
data['event_name'].unique()
Out[18]:
array(['tips_show', 'map', 'search_1', 'photos_show', 'favorites_add',
       'contacts_show', 'contacts_call', 'advert_open', 'search_7',
       'search_5', 'search_4', 'search_6', 'search_3', 'tips_click',
       'search_2', 'show_contacts'], dtype=object)
In [19]:
# Создадим список значений, которые нужно будет заменить
search_replace=['search_1','search_2','search_3','search_4','search_5','search_6','search_7']

# Заменим значения
data.loc[data['event_name'].isin(search_replace),'event_name']='search'
data.loc[data['event_name']== 'show_contacts','event_name']='contacts_show'
In [20]:
data['event_name'].unique()
Out[20]:
array(['tips_show', 'map', 'search', 'photos_show', 'favorites_add',
       'contacts_show', 'contacts_call', 'advert_open', 'tips_click'],
      dtype=object)

Исследовательский анализ данных¶

Количество событий¶

Посмотрим сколько и какого типа событий в данных.

In [21]:
data['event_name'].nunique()
Out[21]:
9
In [22]:
len(data['event_name'])
Out[22]:
74197
In [23]:
data['event_name'].value_counts()
Out[23]:
tips_show        40055
photos_show      10012
search            6784
advert_open       6164
contacts_show     4529
map               3881
favorites_add     1417
tips_click         814
contacts_call      541
Name: event_name, dtype: int64
In [24]:
data.pivot_table(index='event_name',values='user_id', aggfunc='count').sort_values(by='user_id',ascending=False).plot(kind= 'bar')
plt.title("Распределение действий по событиям")
plt.xlabel ('Событие')
plt.ylabel ('Кол-во действий')
plt.xticks(rotation = 45)
plt.legend();

Всего в данных 9 типов и 74197 событий в данных. Больше всего пользователи совершают событие tips_show (увидели рекомендованное объявление), меньше всего пользователи совершают звонки (contacts_call)

Количество пользователей, совершивших события¶

Посмотрим, сколько пользователей в данных и сколько в среднем событий приходится на пользователя.

In [25]:
data['user_id'].nunique()
Out[25]:
4293
In [26]:
data.groupby('user_id')['event_name'].count().describe()
Out[26]:
count    4293.000000
mean       17.283252
std        29.130677
min         1.000000
25%         5.000000
50%         9.000000
75%        17.000000
max       478.000000
Name: event_name, dtype: float64

Среднее кол-во событий на пользователя - 17. Минимальное кол-во 1, максимальное же кол-во 478 (возможно это выбросы). Медианное значение - 9 событий на пользователя.

Посмотрим, сколько пользователей совершали каждое из событий.

In [27]:
events=data.groupby('event_name')['user_id'].nunique().reset_index(name='user_id').sort_values(by='user_id',ascending=False)
events
Out[27]:
event_name user_id
8 tips_show 2801
6 search 1666
4 map 1456
5 photos_show 1095
2 contacts_show 981
0 advert_open 751
3 favorites_add 351
7 tips_click 322
1 contacts_call 213
In [28]:
# Посчитаем долю пользователей, которые хоть раз совершали событие

events['convers'] = round(events['user_id']/len(data['user_id'].unique()), 3) * 100
events
Out[28]:
event_name user_id convers
8 tips_show 2801 65.2
6 search 1666 38.8
4 map 1456 33.9
5 photos_show 1095 25.5
2 contacts_show 981 22.9
0 advert_open 751 17.5
3 favorites_add 351 8.2
7 tips_click 322 7.5
1 contacts_call 213 5.0

Мы видим, что процент конверсии в целевое действие (contacts_show) низкая, всего 22.9%.

Анализ метрик вовлеченности¶

Проведем анализ взаимодействия пользователей, кто пользовался рекомендованными объявлениями и кто переходил по объявлениям самостоятельно.

In [29]:
df=data.copy()
# удалим дубликаты в столбцах
df=df.drop_duplicates(subset={'user_id', 'source','event_name','session_id'})
In [30]:
#сгруппируем датасет по ивентам и создадим колонку с юзер стори
df_1 = df.groupby(['session_id', 'user_id'])['event_name'].apply(list).reset_index(name='event_name').sort_values(by='event_name',ascending=True)
df_1['event_name'] = df_1['event_name'].apply(lambda l: ' '.join(l))
df_2= df_1.groupby(['event_name'])['user_id'].apply(list).reset_index(name='user_id').sort_values(by='event_name',ascending=True)
#Добавим колонку с кол-вом уникальных юзеров
df_2['users_count'] = df_2['user_id'].apply(lambda l: len(set(l)))
df_2.head(5)
Out[30]:
event_name user_id users_count
0 advert_open [e13f9f32-7ae3-4204-8d60-898db040bcfc, 0d5c7fc... 94
1 advert_open contacts_show [9ab0044b-12c3-4fb3-9dec-9050f46397ed, 429d753... 7
2 advert_open contacts_show contacts_call [429d753a-1f31-4819-867e-2c1e36157589, 89fd43c... 4
3 advert_open contacts_show contacts_call photos... [83ae922a-1d12-458a-b591-0ea6d283ce0d] 1
4 advert_open contacts_show map search [b03f4137-440d-4f21-b656-7ed49a545ac0] 1
In [31]:
df_10 = df.groupby(['session_id', 'user_id'])['event_name'].apply(list).reset_index(name='event_name')
df_10['event_name'] = df_10['event_name'].apply(lambda l: ' '.join(l))
df_10
Out[31]:
session_id user_id event_name
0 1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show
1 2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 map tips_show
2 3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show map
3 4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 map tips_show
4 5 00157779-810c-4498-9e05-a1e9e3cedf93 search photos_show
... ... ... ...
10363 10364 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show
10364 10365 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show contacts_show
10365 10366 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show contacts_show
10366 10367 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show contacts_show
10367 10368 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show contacts_show

10368 rows × 3 columns

In [32]:
def DrawFunnelHelper(story, funnelx, funnely, data_set):
    if len(story) == 0:
        return data_set, funnelx, funnely;
    story_step = story[0]
    story.pop(0)

    if story_step[0] == '!':
        #добавим возможность исключать ивенты
        story_step = story_step.replace("!", "")
        filtered = data_set.query('event_name.str.contains(@story_step)')
        users = filtered.user_id.unique()
        sessions = filtered.session_id.unique()
        data_set = data_set.query('session_id not in @sessions')#.query('user_id not in @users')
        funnely.append("no " + story_step)
    else:
        data_set = data_set.query('event_name.str.contains(@story_step)')
        funnely.append(story_step)
    users_count = data_set.user_id.nunique()

    funnelx.append(users_count)
    #рекурсивно фильтруем датасет
    return DrawFunnelHelper(story, funnelx, funnely, data_set)
In [33]:
def DrawFunnel(user_story, data_f, title):
    funnel_x=[]
    funnel_y=[]
    filtered_df, x,y = DrawFunnelHelper(user_story, funnel_x, funnel_y, data_f)
    fig = go.Figure()
    fig.add_trace(go.Funnel(
    y = y,
    x = x,
    textposition = "auto",
    textinfo = "value+percent initial+percent previous",
    
    ))
    fig.update_layout(title=title)   
    fig.show()
    return filtered_df

Построим воронку взаимодействий пользователей, кто пользовался поиском.

In [34]:
new_data = df_10.copy()
fdf = DrawFunnel(['search','contacts_show'], new_data,"Воронка взаимодействий пользователей, кто пользовался поиском")

Вывод:

  • После поиска объявления, смотрят контакты около 17%

Построим воронку тех пользователей, кто пользовался рекомендательной системой.

In [35]:
fdf = DrawFunnel(['tips_show', 'tips_click','contacts_show'], new_data,"Воронка взаимодействий пользователей, кто пользовался рекомендованными объявлениями")

Вывод:

  • Из тех, кто видел рекомендованные объявления, всего 2% пользователей, просмотрели контакты рекомендованных объявлений.

Основные вопросы исследования¶

Поиск сценариев пользователей¶

In [36]:
pd.set_option('max_colwidth', 100)
pd.set_option('display.width', 300)

# создадим датафрейм с группировкой по сессиями и юзерам
grouped_by_user = df.groupby(['session_id', 'user_id'])['event_name'].apply(list).reset_index(name='event_name').sort_values(by='session_id',ascending=True)
grouped_by_user['event_name'] = grouped_by_user['event_name'].apply(lambda l: ' '.join(l))

# выделим сценарии, которые содержат целевое событие
gr=grouped_by_user.query('event_name.str.contains("contacts_show")')
#grouped_by_user
gr_1 = gr.event_name.value_counts().to_dict()
# выделим наиболее популярные сценарии
gr['session_count'] = gr.event_name.apply(lambda x: gr_1[x])

#удалим дубликаты
gr=gr.drop_duplicates(subset='event_name',keep='first').sort_values(by='session_count',ascending=False)

gr.head(10)
Out[36]:
session_id user_id event_name session_count
40 41 0103a07d-513f-42b9-8d91-d5891d5655fe tips_show contacts_show 342
9 10 00157779-810c-4498-9e05-a1e9e3cedf93 contacts_show 195
17 18 00551e79-152e-4441-9cf7-565d7eb04090 contacts_show contacts_call 120
29 30 007d031d-5018-4e02-b7ee-72a30609173f map tips_show contacts_show 94
60 61 01d283e1-cb1c-407a-a4e0-9f72f3deecca photos_show contacts_show 85
22 23 005fbea5-2678-406f-88a6-fbe9787e2268 contacts_show tips_show 74
18 19 00551e79-152e-4441-9cf7-565d7eb04090 search contacts_show contacts_call 54
113 114 03bef3ef-cce8-46ed-8c70-414b6b0486fb search contacts_show 52
8 9 00157779-810c-4498-9e05-a1e9e3cedf93 search photos_show contacts_show 45
127 128 0420f4cf-29ec-44c6-af9c-247f36efea68 contacts_show photos_show 38

Диаграмма Санкея¶

Построим диаграмму Санкея, чтобы убеиться в правильности нахождения сценариев пользователей.

In [37]:
# создадим функцию для генерации новых столбцов для исходной таблицы
def add_features(df):
        
    # сортируем по session_id и времени
    sorted_df = df.sort_values(by=['session_id', 'event_time']).copy()
    # добавляем шаги событий
    sorted_df['step'] = sorted_df.groupby('session_id').cumcount() + 1
    
    # добавляем узлы-источники и целевые узлы
    # узлы-источники - это сами события
    sorted_df['source'] = sorted_df['event_name']
    # добавляем целевые узлы
    sorted_df['target'] = sorted_df.groupby('session_id')['source'].shift(-1)
    
    # возврат таблицы без имени событий
    return sorted_df.drop(['event_name'], axis=1)
In [38]:
new_df = add_features(df)
In [39]:
new_df
Out[39]:
user_id source event_time session_id step target
2171 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show 2019-10-07 13:39:46 1 1 NaN
2180 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 map 2019-10-09 18:33:56 2 1 tips_show
2182 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show 2019-10-09 18:40:29 2 2 NaN
2184 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show 2019-10-21 19:52:31 3 1 map
2186 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 map 2019-10-21 19:53:39 3 2 NaN
... ... ... ... ... ... ...
19021 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 2019-11-02 19:26:08 10366 2 NaN
19024 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show 2019-11-03 14:32:56 10367 1 contacts_show
19025 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 2019-11-03 14:33:48 10367 2 NaN
19039 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show 2019-11-03 15:36:01 10368 1 contacts_show
19044 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 2019-11-03 15:48:05 10368 2 NaN

17854 rows × 6 columns

In [40]:
new_df.groupby('session_id')['source'].nunique().describe()
Out[40]:
count    10368.000000
mean         1.722029
std          0.893418
min          1.000000
25%          1.000000
50%          1.000000
75%          2.000000
max          6.000000
Name: source, dtype: float64
In [41]:
# удалим все пары source-target, шаг которых превышает 6 
df_comp = new_df[new_df['step'] < 6].copy().reset_index(drop=True)
In [42]:
# создадим функцию для генерации индексов source
def get_source_index(df):
  
    res_dict = {}
    
    count = 0
    # получаем индексы источников
    for no, step in enumerate(new_df['step'].unique().tolist()):
        # получаем уникальные наименования для шага
        res_dict[no+1] = {}
        res_dict[no+1]['sources'] = new_df[new_df['step'] == step]['source'].unique().tolist()
        res_dict[no+1]['sources_index'] = []
        for i in range(len(res_dict[no+1]['sources'])):
            res_dict[no+1]['sources_index'].append(count)
            count += 1
            
    # соединим списки
    for key in res_dict:
        res_dict[key]['sources_dict'] = {}
        for name, no in zip(res_dict[key]['sources'], res_dict[key]['sources_index']):
            res_dict[key]['sources_dict'][name] = no
    return res_dict
In [43]:
source_indexes = get_source_index(df_comp)
In [44]:
# создадим функцию для вывода данных для конкретного шага будущей диаграммы

def show_example(step, source_indexes=source_indexes):
    
    print(f'Пример подготовленных данных для шага {step}\n')
    for key in source_indexes[step]:
        print(f'{key}\n', source_indexes[step][key], '\n')
In [45]:
show_example(1)
Пример подготовленных данных для шага 1

sources
 ['tips_show', 'map', 'search', 'photos_show', 'contacts_show', 'advert_open', 'favorites_add', 'tips_click'] 

sources_index
 [0, 1, 2, 3, 4, 5, 6, 7] 

sources_dict
 {'tips_show': 0, 'map': 1, 'search': 2, 'photos_show': 3, 'contacts_show': 4, 'advert_open': 5, 'favorites_add': 6, 'tips_click': 7} 

In [46]:
# создадим функцию для генерации цветов rgba
def colors_for_sources(mode):
  
    # словарь, в который сложим цвета в соответствии с индексом
    colors_dict = {}
    
    if mode == 'random':
        # генерим случайные цвета
        for label in df_comp['source'].unique():
            r, g, b = np.random.randint(255, size=3)            
            colors_dict[label] = f'rgba({r}, {g}, {b}, 1)'
            
    elif mode == 'custom':
        # присваиваем ранее подготовленные цвета
        colors = requests.get('https://raw.githubusercontent.com/rusantsovsv/senkey_tutorial/main/json/colors_senkey.json').json()
        for no, label in enumerate(df_comp['source'].unique()):
            colors_dict[label] = colors['custom_colors'][no]
            
    return colors_dict
In [47]:
colors_dict = colors_for_sources(mode='custom')
In [48]:
# пересчитаем количестов юзеров в процентах от входа
def percent_users(sources, targets, values):
    
    # объединим источники и метки и найдем пары
    zip_lists = list(zip(sources, targets, values))
    
    new_list = []
    
    # подготовим список словарь с общим объемом трафика в узлах
    unique_dict = {}
    
    # проходим по каждому узлу
    for source, target, value in zip_lists:
        if source not in unique_dict:
            # находим все источники и считаем общий трафик
            unique_dict[source] = 0
            for sr, tg, vl in zip_lists:
                if sr == source:
                    unique_dict[source] += vl
                    
    # считаем проценты
    for source, target, value in zip_lists:
        new_list.append(round(100 * value / unique_dict[source], 1))
    
    return new_list
In [49]:
#  сделаем функцию для создания необходимых для отрисовки диаграммы переменных списков

def lists_for_plot(source_indexes=source_indexes, colors=colors_dict, frac=10):

    sources = []
    targets = []
    values = []
    labels = []
    link_color = []
    link_text = []

    # проходим по каждому шагу
    for step in tqdm(sorted(df_comp['step'].unique()), desc='Шаг'):
        if step + 1 not in source_indexes:
            continue

        # получаем индекс источника
        temp_dict_source = source_indexes[step]['sources_dict']

        # получаем индексы цели
        temp_dict_target = source_indexes[step+1]['sources_dict']

        # проходим по каждой возможной паре, считаем количество таких пар
        for source, index_source in tqdm(temp_dict_source.items()):
            for target, index_target in temp_dict_target.items():
                # делаем срез данных и считаем количество id
                temp_df = df_comp[(df_comp['step'] == step)&(df_comp['source'] == source)&(df_comp['target'] == target)]
                value = len(temp_df)
                # проверяем минимальный объем потока и добавляем нужные данные
                if value > frac:
                    sources.append(index_source)
                    targets.append(index_target)
                    values.append(value)
                    # делаем поток прозрачным для лучшего отображения
                    link_color.append(colors[source].replace(', 1)', ', 0.2)'))

    labels = []
    colors_labels = []
    for key in source_indexes:
        for name in source_indexes[key]['sources']:
            labels.append(name)
            colors_labels.append(colors[name])

    # посчитаем проценты всех потоков
    perc_values = percent_users(sources, targets, values)

    # добавим значения процентов для howertext
    link_text = []
    for perc in perc_values:
        link_text.append(f"{perc}%")

    # возвратим словарь с вложенными списками
    return {'sources': sources,
            'targets': targets,
            'values': values,
            'labels': labels,
            'colors_labels': colors_labels,
            'link_color': link_color,
            'link_text': link_text}
In [50]:
data_for_plot = lists_for_plot()
Шаг:   0%|          | 0/5 [00:00<?, ?it/s]
  0%|          | 0/8 [00:00<?, ?it/s]
  0%|          | 0/9 [00:00<?, ?it/s]
  0%|          | 0/9 [00:00<?, ?it/s]
  0%|          | 0/9 [00:00<?, ?it/s]
  0%|          | 0/8 [00:00<?, ?it/s]
In [51]:
# создадим функцию для генерации объекта диаграммы Сенкей 
def plot_senkey_diagram(data_dict=data_for_plot):    
    
    
    fig = go.Figure(data=[go.Sankey(
        domain = dict(
          x =  [0,1],
          y =  [0,1]
        ),
        orientation = "h",
        valueformat = ".0f",
        node = dict(
          pad = 50,
          thickness = 15,
          line = dict(color = "black", width = 0.1),
          label = data_dict['labels'],
          color = data_dict['colors_labels']
        ),
        link = dict(
          source = data_dict['sources'],
          target = data_dict['targets'],
          value = data_dict['values'],
          label = data_dict['link_text'],
          color = data_dict['link_color']
      ))])
    fig.update_layout(title_text="Sankey Diagram", font_size=10, width=1000, height=600)
    
    # возвращаем объект диаграммы
    return fig
In [52]:
senkey_diagram = plot_senkey_diagram()
In [53]:
senkey_diagram.show()

Построим воронки по этим сценарием.

In [54]:
fdf = DrawFunnel(['tips_show','contacts_show'], new_data,"Сценарий 'tips_show'-'contacts_show' ")
In [55]:
fdf = DrawFunnel(['map','tips_show','contacts_show'], new_data,"Сценарий 'map'-'tips_show'-'contacts_show' ")
In [56]:
fdf = DrawFunnel(['photos_show','contacts_show'], new_data,"Сценарий 'photos_show'-'contacts_show' ")
In [57]:
fdf = DrawFunnel(['search','photos_show','contacts_show'], new_data,"Сценарий 'search'-'photos_show'-'contacts_show' ")

Вывод:

Наиболее популярными сценариями действий пользователя, приводящих к целевому событию являются:

  • tips_show-contacts_show. Конверсия при сценарии tips_show - contacts_show составляет 18%.
  • map-tips_show-contacts_show. Конверсия при сценарии map-tips_show-contacts_show составляет 14%.
  • photo_show-contacts_show. Конверсия при сценарии photos_show-contacts_show 24%.
  • search-photos_show-contacts_show. Конверсия при сценарии search-photos_show-contacts_show составляет 7%.

На основе анализа можно сказать, что пользователи чаще просматривают контакты из рекомендованных объявлений и если просматривают фотографии.

Анализ влияния событий на совершение целевого события¶

Проанализируем, как различается время между событиями ADVERT_OPEN -> CONTACTS_SHOW И TIPS_CLICK -> CONTACTS_SHOW.Какая конверсия в целевое событие у данных действий?

Среднее время между событиями ADVERT_OPEN -> CONTACTS_SHOW¶

Рассмотрим, как различается время между событиями ADVERT_OPEN -> CONTACTS_SHOW

In [58]:
# выделим сессии, в которых пользователи совершали advert_open и время первого совершения
advert=df[df['event_name'] == "advert_open"].groupby('user_id',as_index=False).agg(first_time=('event_time','min'))
# найдем уникальных пользователей
advert_users=advert['user_id'].nunique()
advert_users
Out[58]:
751
In [59]:
# выделим сессии, в которых пользователи совершали contacts_show
contacts = (df[df['event_name'] == "contacts_show"].merge(advert,on='user_id',how='left'))
#отфильтруем события, случившиеся после advert_name
contacts=contacts[contacts['event_time']>contacts['first_time']]
# найдем уникальных пользователей
contacts_users=contacts['user_id'].nunique()
contacts_users
Out[59]:
109
In [60]:
# найдем конверсию
advert_conver=contacts_users/advert_users
advert_conver
Out[60]:
0.14513981358189082

Конверсия в целевое событие 14.5%

Далее найдем разницу между событиями ADVERT_OPEN -> CONTACTS_SHOW

In [61]:
#создадми датасет со временем всех ивентов
grouped_by_time = df.groupby(['session_id', 'user_id'])['event_time'].apply(list).reset_index(name='event_time').sort_values(by='session_id',ascending=True)

#создадим датасет с именем ивентов
grouped_by_events = df.groupby(['session_id', 'user_id'])['event_name'].apply(list).reset_index(name='event_name').sort_values(by='session_id',ascending=True)

#добавим колонку со сременем в датасет
grouped_by_time['event_name'] = grouped_by_events['event_name']

grouped_by_time.head(5)
Out[61]:
session_id user_id event_time event_name
0 1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 [2019-10-07 13:39:46] [tips_show]
1 2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 [2019-10-09 18:33:56, 2019-10-09 18:40:29] [map, tips_show]
2 3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 [2019-10-21 19:52:31, 2019-10-21 19:53:39] [tips_show, map]
3 4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 [2019-10-22 11:18:15, 2019-10-22 11:19:11] [map, tips_show]
4 5 00157779-810c-4498-9e05-a1e9e3cedf93 [2019-10-19 21:34:34, 2019-10-19 21:40:39] [search, photos_show]
In [62]:
# выделим сессии, в которых пользователи совершали advert_open
advert_session=df.query('event_name == "advert_open"')['session_id']

# выделим сессии, в которых пользователи совершали contacts_show
contacts_show_session = df.query('event_name == "contacts_show"')['session_id']

# найдем сессии, в которых были все 2 типа действий
advert = set(advert_session) & set(contacts_show_session)
In [63]:
#создадим фукнцию, которая будет создавать словарь event_time-event_name
def mergeEventTime(ser):
    return dict(zip(ser[0], ser[1]))
In [64]:
#создадим датасет с нужными сессиями
advert_data = grouped_by_time.query('session_id in @advert')

#создадим колонку со списком ивенты-время
advert_data['time_event']=list(zip(advert_data.event_name, advert_data.event_time))

#смержим два списка в словарь ивент-время
advert_data['time_event'] = advert_data.time_event.apply(mergeEventTime)
In [65]:
#создадим колонки start_time и end_time с нужным временм из словаря
advert_data['start_time'] = advert_data.time_event.apply(lambda x: x['advert_open'])
advert_data['end_time'] = advert_data.time_event.apply(lambda x: x['contacts_show'])

#приведем колонки к типу datetime
advert_data['start_time']=pd.to_datetime(advert_data['start_time'], format='%Y-%m-%d %H:%M:%S').dt.round('1S')
advert_data['end_time']=pd.to_datetime(advert_data['end_time'], format='%Y-%m-%d %H:%M:%S').dt.round('1S')

#вычислим разницу
advert_data['duration'] = advert_data['end_time'] - advert_data['start_time']

# переведем значения в секунды
advert_data['duration']=advert_data['duration'].dt.total_seconds()

#отбросим отрицательные значения
advert_data=advert_data.query("duration >= 0")
In [66]:
advert_data.describe()
Out[66]:
session_id duration
count 97.000000 97.000000
mean 4929.701031 481.649485
std 2878.669442 741.526270
min 151.000000 3.000000
25% 2246.000000 61.000000
50% 5282.000000 178.000000
75% 7103.000000 592.000000
max 10252.000000 4251.000000

Медианное время между действиями advert_open и contacts_show около 178 секунды (около 3 минут).

Среднее время между событиями TIPS_CLICK -> CONTACTS_SHOW¶

In [67]:
# выделим сессии, в которых пользователи совершали advert_open и время первого совершения
tips_click=df[df['event_name'] == "tips_click"].groupby('user_id',as_index=False).agg(first_time=('event_time','min'))
# найдем уникальных пользователей
tips_users=tips_click['user_id'].nunique()
tips_users
Out[67]:
322
In [68]:
# выделим сессии, в которых пользователи совершали contacts_show
tips_contacts = (df[df['event_name'] == "contacts_show"].merge(tips_click,on='user_id',how='left'))
#отфильтруем события, случившиеся после advert_name
tips_contacts=tips_contacts[tips_contacts['event_time']>tips_contacts['first_time']]
# найдем уникальных пользователей
tips_contacts_users=tips_contacts['user_id'].nunique()
tips_contacts_users
Out[68]:
59
In [69]:
# найдем конверсию
tips_conver=tips_contacts_users/tips_users
tips_conver
Out[69]:
0.18322981366459629

Конверсия в целевое событие 18.3%

In [70]:
# Найдем сессии, в которых пользователь совершал tips_click
tips_session = df.query('event_name == "tips_click"')['session_id'].unique()

# найдем сессии, в которых были все 2 типа действий
tips = set(tips_session) & set(contacts_show_session)
In [71]:
#создадим датасет с нужными сессиями
tips_data = grouped_by_time.query('session_id in @tips')

#создадим колонку со списком ивенты-время
tips_data['time_event']=list(zip(tips_data.event_name, tips_data.event_time))

#смержим два списка в словарь ивент-время
tips_data['time_event'] = tips_data.time_event.apply(mergeEventTime)
In [72]:
#создадим колонки start_time и end_time с нужным временм из словаря
tips_data['start_time'] = tips_data.time_event.apply(lambda x: x['tips_click'])
tips_data['end_time'] = tips_data.time_event.apply(lambda x: x['contacts_show'])

#приведем колонки к типу datetime
tips_data['start_time']=pd.to_datetime(tips_data['start_time'], format='%Y-%m-%d %H:%M:%S').dt.round('1S')
tips_data['end_time']=pd.to_datetime(tips_data['end_time'], format='%Y-%m-%d %H:%M:%S').dt.round('1S')

#вычислим разницу
tips_data['duration'] = tips_data['end_time'] - tips_data['start_time']

# переведем значения в секунды
tips_data['duration']=tips_data['duration'].dt.total_seconds()

#отбросим отрицательные значения
tips_data=tips_data.query("duration >= 0")
In [73]:
tips_data.describe()
Out[73]:
session_id duration
count 28.000000 28.0000
mean 5155.357143 815.7500
std 2976.691156 808.6481
min 538.000000 18.0000
25% 1904.250000 235.0000
50% 4889.000000 471.0000
75% 7499.250000 1194.2500
max 10022.000000 3048.0000

Среднее время между действиями tips_click и contacts_show около 471 секунды (около 7 минут).

Вывод¶

  1. Наиболее популярными сценариями являются:
  • tips_show-contacts_show (342 сессии)
  • map-tips_show-contacts_show (94 сессии)
  • photo_show-contacts_show (85 сессий)
  • search-contacts_show (52 сессии)
  • search-photo_show-contacts_show (45 сессий)
  1. Пользователь при открытии рекомендованного объявления проводит в карточке объявления около 7 минут и потом нажимает на показ контактов, конверсия 18.3%.

    При открытии объявления, кроме рекомендованного, пользователь проводит 3 минут и конверсия в просмотр контактов составляет 14.5%.

    Конверсия в целевое событие пользователей, которые самостояльно нашли объявление выше.

Формулировка гипотез¶

Одни пользователи совершают действия tips_show и tips_click, другие — только tips_show. Проверьте гипотезу: конверсия в просмотры контактов различается у этих двух групп.¶

In [74]:
click_df = DrawFunnel(['tips_show', 'tips_click'], new_data, 'tips_show-tips_click')
click_show_df = DrawFunnel(['tips_show', 'tips_click','contacts_show'], new_data, 'tips_show-tips_click-contacts_show')
no_click_df = DrawFunnel(['tips_show', '!tips_click'], new_data, 'tips_show-!tips_click')
no_click_show_df = DrawFunnel(['tips_show', '!tips_click','contacts_show'], new_data, 'tips_show-!tips_click-contacts_show')
In [75]:
# КОД РЕВЬЮЕРА

df.query('event_name == "tips_show"')['user_id'].nunique()
Out[75]:
2801
In [76]:
no_click_count = no_click_df.user_id.nunique()
click_count = click_df.user_id.nunique()
no_click_show_count = no_click_show_df.user_id.nunique()
click_show_count = click_show_df.user_id.nunique()

Определим гипотезы:

H0- Статистически значимой разницы в конверсии между контрольными нет.

H1- Статистически значимая разница в конверсии между конрольными группами есть.

In [77]:
alpha = 0.05  # критический уровень статистической значимости

target_action= np.array([no_click_count,click_count])
user_count = np.array([no_click_show_count, click_show_count] )

print(user_count, target_action) # КОД РЕВЬЮЕРА!!

# пропорция успехов в первой группе:
p1 = user_count[0]/target_action[0]

# пропорция успехов во второй группе:
p2 = user_count[1]/target_action[1]

# пропорция успехов в комбинированном датасете:
p_combined = (user_count[0] + user_count[1]) / (target_action[0] + target_action[1])

# разница пропорций в датасетах
difference = p1 - p2 

z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/target_action[0] + 1/target_action[1]))

# задаем стандартное нормальное распределение 
distr = st.norm(0, 1) 

p_value = (1 - distr.cdf(abs(z_value))) * 2

print('p-значение: ', p_value)

if p_value < alpha:# ваш код
    print('Отвергаем нулевую гипотезу: между конверсиями есть значимая разница')
else:
    print(
        'Не получилось отвергнуть нулевую гипотезу, нет оснований считать конверсии разными'
    )
[459  63] [2691  291]
p-значение:  0.05017555576325239
Не получилось отвергнуть нулевую гипотезу, нет оснований считать конверсии разными

По полученным исследованиям, мы можем сделать вывод, что нет оснований считать, что конверсии разные.

Конверсия для пользователей только tips_show:

In [78]:
no_click_show_count/no_click_count
Out[78]:
0.1705685618729097

Конверсия для пользователей tips_show и tips_click:

In [79]:
click_show_count/click_count
Out[79]:
0.21649484536082475

Вывод:

Пользователи, которые совершают действия tips_show и tips_click имеют конверсию в целевое действие около 21.6%, пользователи же, которые совершают только tips_show 16.2%, что может говорить о том, что пользователи,просматривающие рекомендованные объявления чаще просматривают контакты.

Одни пользователи просматривают фотографии photos_show, другие нет. Проверить гипотезу: пользователи, которые просматривают фотографии, чаще звонят по номеру из объявления (конверсия в звонки у групп различается).¶

In [80]:
# КОД РЕВЬЮЕРА

# формируем датафрейм с заданным действием и первое время его совершения
photo_event_df = (df[df['event_name'] == 'photos_show']
                                .groupby('user_id', as_index=False)
                                .agg(photo_event_time=('event_time', 'min')))

# количество пользователей которые совершили photos_show
photo_event_df['user_id'].nunique()
Out[80]:
1095
In [81]:
# КОД РЕВЬЮЕРА

# находим пользователей которые совершили контакты и мерджим с предыдущим датафреймом
call_user_df = (df[df['event_name'] == 'contacts_call']
                                .merge(photo_event_df, on='user_id', how='left'))

# фильтруем, чтобы остались только те целевые события, что были после tips_click
call_user_df = call_user_df[call_user_df['event_time'] > call_user_df['photo_event_time']]

# считаем сколько пользователей
call_user_df['user_id'].nunique()
Out[81]:
127

In [82]:
# создадим датайфрейм с пользовпателями, которые совершали photos_show
photo_event_df = (df[df['event_name'] == 'photos_show']
                                .groupby('user_id', as_index=False)
                                .agg(photo_event_time=('event_time', 'min')))

# находим пользователей которые совершили звонки и мерджим с предыдущим датафреймом
call_user_df = (df[df['event_name'] == 'contacts_call']
                                .merge(photo_event_df, on='user_id', how='left'))

# фильтруем, чтобы остались только те целевые события, что были после photow_show
call_user_df = call_user_df[call_user_df['event_time'] > call_user_df['photo_event_time']]
In [83]:
# создадим лист с теми, кто совершал photos_show
photos_show=photo_event_df['user_id'].unique().tolist()

# созадим датайфрейм с теми, кто не совершал photos_show
no_photo_event_df=(df.query('user_id not in @photos_show')
                                .groupby('user_id', as_index=False)
                                .agg(no_photo_event_time=('event_time', 'min')))

# создадим лист с теми, кто совершал contacts_call
no_call=call_user_df['user_id'].unique().tolist()

# находим пользователей которые не совершили звонки и мерджим с предыдущим датафреймом
no_call_user_df = (df[df['event_name'] == 'contacts_call']
                                .merge(no_photo_event_df, on='user_id', how='left'))

# фильтруем, чтобы остались только те пользователи, которые не совершали contacts_call
no_call_user_df=no_call_user_df.query('user_id not in @no_call')
In [84]:
photos_count = photo_event_df.user_id.nunique()
no_photos_count = no_photo_event_df.user_id.nunique()
photos_call_count = call_user_df.user_id.nunique()
no_photos_call_count = no_call_user_df.user_id.nunique()

Определим гипотезы:

H0- Статистически значимой разницы в конверсии между контрольными нет.

H1- Статистически значимая разница в конверсии между конрольными группами есть.

In [85]:
alpha = 0.05  # критический уровень статистической значимости

user_count = np.array([photos_call_count,no_photos_call_count])
target_action = np.array([photos_count, no_photos_count] )

print(user_count, target_action) # КОД РЕВЬЮЕРА!!

# пропорция успехов в первой группе:
p1 = user_count[0]/target_action[0]

# пропорция успехов во второй группе:
p2 = user_count[1]/target_action[1]

# пропорция успехов в комбинированном датасете:
p_combined = (user_count[0] + user_count[1]) / (target_action[0] + target_action[1])

# разница пропорций в датасетах
difference = p1 - p2 

z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/target_action[0] + 1/target_action[1]))

# задаем стандартное нормальное распределение 
distr = st.norm(0, 1) 

p_value = (1 - distr.cdf(abs(z_value))) * 2

print('p-значение: ', p_value)

if p_value < alpha:# ваш код
    print('Отвергаем нулевую гипотезу: между конверсиями есть значимая разница')
else:
    print(
        'Не получилось отвергнуть нулевую гипотезу, нет оснований считать конверсии разными'
    )
[127  86] [1095 3198]
p-значение:  0.0
Отвергаем нулевую гипотезу: между конверсиями есть значимая разница

Конверсия для тех, кто совершал photos_show

In [86]:
photos_call_count/photos_count
Out[86]:
0.11598173515981736

Конверсия для тех, кто не совершал photos_show

In [87]:
no_photos_call_count/no_photos_count
Out[87]:
0.02689180737961226

Вывод: Пользователи, которые просматривали фото имеют конверсию в целевое действие 11.5%, те, кто не смотрел фото имеют конверсию в 2.6%. Разница большая, что говорит о том, что просмотр фото увеличивает вероятность звонка.

Вывод¶

В анализе мы использовали данные пользователей, впервые совершивших действия в приложении после 7 октября 2019 года.

Мы провели следующие исследования и сделали выводы:

  1. Изучение и предобработка данных

    1. У нас было 2 датасета ( mobile_dataset и mobile_sources). В них мы поменяли названия столбов и объединили в единый датасет.
    2. Заменили типы данных в столбце даты
    3. Проверили на пропуски (пропусков в датасете нет)
    4. В данных оказалось 1118 дубликатов, возможно, они могли возникнуть после объединения столбов, мы их оставили для корректности исследования.
    5. Создали столбец session_id, выделив тайм-аут времени в 30 мин.
    6. Заменили значения в столбцах search и contacts_show
  2. Исследовательский анализ данных

    1. Всего в данных 9 типов и 74197 событий в данных. Больше всего пользователи совершают событие tips_show (увидели рекомендованное объявление), меньше всего пользователи совершают звонки (contacts_call)
    2. Среднее кол-во событий на пользователя - 17. Минимальное кол-во 1, максимальное же кол-во 478 (возможно это выбросы). Медианное значение - 9 событий на пользователя.
    3. Процент конверсии в целевое действие (contacts_show) низкая, всего 22.9%.

Провели анализ взаимодействия пользователей, кто пользовался рекомендованными объявлениями и кто переходил по объявлениям самостоятельно, получили следующие выводы:

Для тех пользователей, кто пользовался поиском, конверсия в просмотры контактов около 17%

Для тех пользователей, кто переходил по рекомендованным объявлениям, всего 2% пользователей, просмотрели контакты рекомендованных объявлений.

  1. Сценарии пользователей

Мы провели поиск сценариев пользователей и построили диаграмму Санкея и получили следующие выводы:

  1. Наиболее популярными сценариями действий пользователя являются:
  • tips_show-contacts_show. Конверсия при сценарии tips_show - contacts_show составляет 18%.
  • map-tips_show-contacts_show. Конверсия при сценарии map-tips_show-contacts_show составляет 14%.
  • photo_show-contacts_show. Конверсия при сценарии photos_show-contacts_show 24%.
  • search-photos_show-contacts_show. Конверсия при сценарии search-photos_show-contacts_show составляет 7%.

Сделали анализ влияния событий на совершение целевого события:

ADVERT_OPEN -> CONTACTS_SHOW

  1. Конверсия в целевое событие 14,5%
  2. Медианное время между действиями advert_open и contacts_show около 178 секунды (около 3 минут).

TIPS_CLICK -> CONTACTS_SHOW

  1. Конверсия в целевое событие 18.3%
  2. Среднее время между действиями tips_click и contacts_show около 471 секунды (около 7 минут).

Конверсия в целевое событие пользователей, которые перешли по рекомендациям.

  1. Формулировка гипотез
  1. Проверили 1 гипотезу: Одни пользователи совершают действия tips_show и tips_click, другие — только tips_show. Конверсия в просмотры контактов различается у этих двух групп. Получили следующие выводы:

Пользователи, которые совершают действия tips_show и tips_click имеют конверсию в целевое действие около 21.6%, пользователи же, которые совершают только tips_show 16.2%, что может говорить о том, что пользователи,просматривающие рекомендованные объявления чаще просматривают контакты.

  1. Проверили 2 гипотезу: Одни пользователи просматривают фотографии photos_show, другие нет. Пользователи, которые просматривают фотографии, чаще звонят по номеру из объявления (конверсия в звонки у групп различается).Получили следующие выводы:

Пользователи, которые просматривали фото имеют конверсию в целевое действие 11.5%, те, кто не смотрел фото имеют конверсию в 2.6%. Разница большая, что говорит о том, что просмотр фото увеличивает вероятность звонка.

На основе анализа можно сказать, что пользователи чаще просматривают контакты из рекомендованных объявлений и если просматривают фотографии.

Общий вывод:

Лучше всего в целевое событие конвертируются события, которые пользователи смотрят из рекомендованных объявлений.Также наиболее популярными сценариями являются сценарии с рекомендованными объявлениями и просмотром фото. Чаще звонят пользователи, которые просмотрели фото. Заказчику стоит улучшать карточку объявления и улучшать рекомендательную систему в приложении.

Ссылка на презентацию: https://disk.yandex.ru/i/nCeXQU-vB2aBfw